#import "draw.h"

/* See the Dragging.rtf for overview of Dragging in Draw. */

#define ERROR -1

@implementation GraphicView(Drag)

/*
 * Determines whether there is a Graphic at the specified point that is
 * willing to accept a dragged color.  If color is non-NULL, then the
 * color is actually set in that Graphic (i.e. you can find out if any
 * Graphics is "willing" to accept a color by calling this with
 * color == NULL).
 *
 * We use the mechanism of sending a Graphic the message colorAcceptorAt:
 * and letting it return a Graphic (rather than just asking each Graphic
 * doYouAcceptAColorAt:) so that Group's of Graphics can return one of
 * the Graphic's inside itself as the one to handle a dropped color.
 */

- (BOOL)acceptsColor:(NXColor *)color atPoint:(const NXPoint *)point
{
    id change;
    NXRect gbounds;
    Graphic *graphic;
    int i, count = [glist count];

    if (!point) return NO;

    for (i = 0; i < count; i++) {
	graphic = [glist objectAt:i];
	if (graphic = [graphic colorAcceptorAt:point]) {
	    if (color) {
		[graphic getExtendedBounds:&gbounds];
		change = [[FillGraphicsChange alloc] initGraphicView:self forChangeToGraphic:graphic];
		[change startChange];
		    [graphic setFillColor:color];
		    [self cache:&gbounds];		// acceptColor:atPoint:
		    [window flushWindow];
		[change endChange];
		return YES;
	    } else {
		return YES;
	    }
	}
    }

    return NO;
}

/* Just counts the number of Pasteboard types in types. */

static int countTypes(const NXAtom *types)
{
    int count = 0;
    while (types && *types) {
	count++;
	types++;
    }
    return count;
}

/*
 * Registers the view with the Workspace Manager so that when the
 * user picks up an icon in the Workspace and drags it over our view
 * and lets go, dragging messages will be sent to our view.
 * We register for NXFilenamePboardType because we handle data link
 * files and NXImage and Text files dragged into draw as well as any
 * random file when the Control key is depressed (indicating a link
 * operation) during the drag.  We also accept anything that NXImage
 * is able to handle (even if it's not in a file, i.e., it's directly
 * in the dragged Pasteboard--unusual, but we can handle it, so why
 * not?).
 */

- registerForDragging
{
    const NXAtom *pboardImageTypes;
    [self registerForDraggedTypes:&NXFilenamePboardType count:1];
    [self registerForDraggedTypes:&NXColorPboardType count:1];
    pboardImageTypes = [NXImage imagePasteboardTypes];
    [self registerForDraggedTypes:pboardImageTypes count:countTypes(pboardImageTypes)];
    return self;
}

/*
 * This is where we determine whether the contents of the dragging Pasteboard
 * is acceptable to Draw.  The gvFlags.drag*Ok flags say whether we can accept
 * the dragged information as a result of a copy or link (or both) operation.
 * If NXImage can handle the Pasteboard, then we know we can do copy.  We
 * always know we can do link as long as we have a linkManager.  We cache as
 * much of the answer around as we can so that draggingUpdated: will be fast.
 * Of course, we can't cache the part of the answer which is dependent upon
 * the position inside our view (important for colors).
 */

- (NXDragOperation)draggingEntered:(id <NXDraggingInfo>)sender
{
    Pasteboard *pboard;
    NXDragOperation sourceMask;

    gvFlags.dragCopyOk = NO;
    gvFlags.dragLinkOk = NO;

    sourceMask = [sender draggingSourceOperationMask];
    pboard = [sender draggingPasteboard];

    if (IncludesType([pboard types], NXColorPboardType)) {
	NXPoint p = [sender draggingLocation];
	[self convertPoint:&p fromView:nil];
	if ([self acceptsColor:NULL atPoint:&p]) return NX_DragOperationGeneric;
    } else if (sourceMask & NX_DragOperationCopy) {
	if ([NXImage canInitFromPasteboard:pboard]) {
	    gvFlags.dragCopyOk = YES;
	} else if (IncludesType([pboard types], NXFilenamePboardType)) {
	    gvFlags.dragCopyOk = YES;
	}
    }

    if (linkManager) gvFlags.dragLinkOk = YES;

    if (sourceMask & NX_DragOperationCopy) {
	if (gvFlags.dragCopyOk) return NX_DragOperationCopy;
    } else if (sourceMask & NX_DragOperationLink) {
	if (gvFlags.dragLinkOk) return NX_DragOperationLink;
    }

    return NX_DragOperationNone;
}

/*
 * This is basically the same as draggingEntered: but, instead of being
 * called when the dragging enters our view, it is called every time the
 * mouse moves while dragging inside our view.
 */

- (NXDragOperation)draggingUpdated:(id <NXDraggingInfo>)sender
{
    NXDragOperation sourceMask = [sender draggingSourceOperationMask];
    if (IncludesType([[sender draggingPasteboard] types], NXColorPboardType)) {
	NXPoint p = [sender draggingLocation];
	[self convertPoint:&p fromView:nil];
	if ([self acceptsColor:NULL atPoint:&p]) return NX_DragOperationGeneric;
    } else if (sourceMask & NX_DragOperationCopy) {
	if (gvFlags.dragCopyOk) return NX_DragOperationCopy;
    } else if (sourceMask & NX_DragOperationLink) {
	if (gvFlags.dragLinkOk) return NX_DragOperationLink;
    }
    return NX_DragOperationNone;
}

#define FILE_ICON_OR_LINK_BUTTON NXLocalString("Do you want the link to this file to appear as a Link Button or as the icon of the file?", NULL, "Question asked of a user when he drags a file icon into Draw.")
#define FILE_ICON NXLocalizedString("File Icon", NULL, "Button choice if user wants a file dragged into Draw to appear as an icon.")
#define LINK_BUTTON NXLocalizedString("Link Button", NULL, "Button choice if user wants a file dragged into Draw to appear as a link button.")
#define FILE_CONTENTS_OR_ICON_OR_LINK_BUTTON NXLocalizedString("Do you want the contents of this file to appear in Draw, or would you like to create only a link to this data, in which case, do you want the link to appear as a Link Button or as the icon of the file?", NULL, "Question asked of a user when he drags a file which can be imaged directly in Draw into Draw.")
#define FILE_CONTENTS NXLocalizedString("File Contents", NULL, "Button choice if user wants a file dragged into Draw to appear as the contents of the file.")

#define BAD_IMAGE NXLocalString("Unable to import that image into Draw.", NULL, "Message of alert which lets the user know that an image (PostScript or TIFF or something) that he tried to import was some how defective.")

/*
 * Takes the name of a saved link (.objlink) file and incorporates
 * the "linked thing" into the view.  This is really just some glue between
 * the dragging mechanism and the addLink:toGraphic:at:update: method
 * which does all the work of actually incorporating the linked stuff
 * into the view.
 */

- (int)createGraphicForDraggedLink:(const char *)file at:(const NXPoint *)p
{
    NXDataLink *link;
    Graphic *graphic = nil;

    if (linkManager) {
	link = [[NXDataLink alloc] initFromFile:file];
	if ([self addLink:link toGraphic:graphic at:p update:UPDATE_IMMEDIATELY]) {
	    return YES;
	} else {
	    // addLink: frees the link if there's an error
	    NXRunAlertPanel(NULL, BAD_IMAGE, NULL, NULL, NULL);
	    return ERROR;
	}
    }

    return NO;
}

/* A couple of convenience methods to determine what kind of file we have. */

static BOOL isNXImageFile(const char *file)
{
    const char *extension = strrchr(file, '.');
    return (extension && [NXImage imageRepForFileType:extension+1]) ? YES : NO;
}

static BOOL isRTFFile(const char *file)
{
    const char *extension = strrchr(file, '.');
    return (extension && !strcmp(extension, ".rtf")) ? YES : NO;
}

/*
 * Creates a Graphic from a file NXImage or the Text object can handle
 * (or just allows linking it if NXImage nor Text can handle the file).
 * It links to it if the doLink is YES.
 *
 * If we are linking, then we ask the user if she wants the file's icon,
 * a link button, or (if we can do so) the actually contents of the file
 * to appear in the view.
 *
 * Note the use of the workspace protocol object to get information about
 * the file.  We know that we cannot import the contents of a WriteNow or
 * other known document format into Draw, so we don't even give the user
 * the option of trying to do so.
 *
 * Again, if it ends up that we are linking, we just call the all-powerful
 * addLink:toGraphic:at:update: method in gvLinks.m, otherwise, we just
 * call placeGraphic:at:.
 */

- (int)createGraphicForDraggedFile:(const char *)file withIcon:(NXImage *)icon at:(const NXPoint *)p andLink:(BOOL)doLink
{
    NXAtom fileType;
    Graphic *graphic;
    NXDataLink *link;
    int choice, updateMode = UPDATE_NORMALLY;
    BOOL isImportable;

    isImportable = isNXImageFile(file) || isRTFFile(file);
    if (!isImportable && [[Application workspace] getInfoForFile:file application:NULL type:&fileType]) {
	isImportable = (fileType == NXPlainFileType);
    }

    if (!linkManager) doLink = NO;

    if (doLink) {
	if (isImportable) {
	    choice = NXRunAlertPanel(NULL, FILE_CONTENTS_OR_ICON_OR_LINK_BUTTON, FILE_CONTENTS, FILE_ICON, LINK_BUTTON);
	} else {
	    choice = NXRunAlertPanel(NULL, FILE_ICON_OR_LINK_BUTTON, FILE_ICON, LINK_BUTTON, NULL);
	    if (choice == NX_ALERTDEFAULT) {
		choice = NX_ALERTALTERNATE;
	    } else if (choice == NX_ALERTALTERNATE) {
		choice = NX_ALERTOTHER;
	    }
	}
    } else if (isImportable) {
	choice = NX_ALERTDEFAULT;
    } else {
	return NO;
    }

    if (choice == NX_ALERTDEFAULT) {		// import the contents of the file
	if (isNXImageFile(file)) {
	    graphic = [[Image allocFromZone:[self zone]] initFromFile:file];
	} else {
	    graphic = [[TextGraphic allocFromZone:[self zone]] initFromFile:file];
	}
	[icon free];
	updateMode = UPDATE_NORMALLY;
    } else if (choice == NX_ALERTALTERNATE) {	// show the file's icon
	graphic = [[Image allocFromZone:[self zone]] initFromIcon:icon];
	updateMode = UPDATE_NEVER;
    } else {					// show a link button
	graphic = [[Image allocFromZone:[self zone]] initWithLinkButton];
	[icon free];
	updateMode = UPDATE_NEVER;
    }

    if (graphic) {
	if (doLink) {
	    link = [[NXDataLink alloc] initLinkedToFile:file];
	    if ([self addLink:link toGraphic:graphic at:p update:UPDATE_NORMALLY]) return YES;
	} else {
	    if ([self placeGraphic:graphic at:p]) return YES;
	}
    }

    NXRunAlertPanel(NULL, BAD_IMAGE, NULL, NULL, NULL);

    return ERROR;
}
 
/*
 * If we get this far, we are pretty sure we can succeed (though we're
 * not 100% sure because it might be, for example, a WriteNow file
 * without the link button down).
 *
 * We return YES here and do the work in conclude so that we don't
 * timeout if we are dragging in a big image or something that will
 * take a long time to import (or if we have to ask the user a question
 * to figure out how to import the dragged thing).  The bummer here
 * is that if creating the image should fail for some reason, we can't
 * give the slide-back feedback because it's too late to do so in
 * concludeDragOperation:.
 */

- (BOOL)performDragOperation:(id <NXDraggingInfo>)sender
{
    Pasteboard *pboard = [sender draggingPasteboard];

    if (IncludesType([pboard types], NXColorPboardType)) {
	BOOL retval = NO;
	NXColor color = NXReadColorFromPasteboard(pboard);
	NXPoint p = [sender draggingLocation];
	[self convertPoint:&p fromView:nil];
	retval = [self acceptsColor:&color atPoint:&p];
	[NXApp updateWindows];	// reflect color change in Inspector, et. al.
	return retval;
    }

    return YES;
}

/* Another convenience method for identifying .objlink files. */

static BOOL isLinkFile(const char *file)
{
    const char *extension = strrchr(file, '.');
    return (extension && !strcmp(extension+1, NXDataLinkFilenameExtension)) ? YES : NO;
}

/*
 * Actually do the "drop" of the drag here.
 *
 * Note that if we successfully dropped, we bring the window
 * we dropped into to the front and activate ourselves.  We also
 * update our inspectors, etc. (this is especially important for
 * the LinkInspector window!) by calling updateWindows.
 */

- concludeDragOperation:(id <NXDraggingInfo>)sender
{
    NXPoint p;
    int length;
    Pasteboard *pboard;
    char *data, *file, *tab;
    int foundOne = NO;
    BOOL doLink;

    p = [sender draggingLocation];
    [self convertPoint:&p fromView:nil];

    doLink = ([self draggingUpdated:sender] == NX_DragOperationLink);
    pboard = [sender draggingPasteboard];

    if (IncludesType([pboard types], NXFilenamePboardType)) {
	[pboard readType:NXFilenamePboardType data:&data length:&length];
	file = data;
	while (file) {
	    if (tab = strchr(file, '\t')) *tab = '\0';
	    if (isLinkFile(file)) {
		foundOne = [self createGraphicForDraggedLink:file at:&p] || foundOne;
	    } else {
		foundOne = [self createGraphicForDraggedFile:file withIcon:[sender draggedImageCopy] at:&p andLink:doLink] || foundOne;
	    }
	    file = tab ? ++tab : NULL;
	}
	[pboard deallocatePasteboardData:data length:length];
    }

    if (!foundOne) foundOne = [self pasteForeignDataFromPasteboard:pboard andLink:doLink at:&p];
    
    if (foundOne > 0) {
	[NXApp activateSelf:YES];
	[window makeKeyAndOrderFront:self];
	[NXApp updateWindows];
    }

    return self;
}

@end
